package util.mongodb;

import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jongo.Jongo;
import org.jongo.Mapper;
import org.jongo.MongoCollection;
import org.jongo.marshall.jackson.JacksonMapper;
import util.mongodb.document.MongoDBDocument;
import util.mongodb.exception.DuplicateDocumentException;
import util.mongodb.exception.NoSuchDocumentException;

import java.io.IOException;
import java.io.StringWriter;
import java.util.*;

/**
 * @author Gregory Boissinot
 */
public class MongoDBDocumentTemplate<D extends MongoDBDocument<D>, I extends D> {

    private final Jongo jongo;
    private final Class<? extends D> documentClass;
    private final String collectionName;
    private final ObjectMapper objectMapper;

    public MongoDBDocumentTemplate(
            MongoDBDataSource mongoDBDataSource,
            SerializationInfo<I> serializationInfo,
            Class<? extends D> documentClass,
            String collectionName) {
        this.jongo = new Jongo(mongoDBDataSource.getMongoDB(), getJacksonMapper(serializationInfo));
        this.documentClass = documentClass;
        this.collectionName = collectionName;
        this.objectMapper = new ObjectMapper();
    }

    private Mapper getJacksonMapper(SerializationInfo<I> serializationInfo) {
        final JacksonMapper.Builder jacksonMapperBuilder = new JacksonMapper.Builder();

        final Map<Class<I>, JsonDeserializer<I>> deserializerMap = serializationInfo.getDeserializerMap();
        for (Map.Entry<Class<I>, JsonDeserializer<I>> jsonDeserializerEntry : deserializerMap.entrySet()) {
            jacksonMapperBuilder.addDeserializer(jsonDeserializerEntry.getKey(), jsonDeserializerEntry.getValue());
        }

        final Map<Class<I>, JsonSerializer<I>> serializerMap = serializationInfo.getSerializerMap();
        for (Map.Entry<Class<I>, JsonSerializer<I>> jsonDeserializerEntry : serializerMap.entrySet()) {
            jacksonMapperBuilder.addSerializer(jsonDeserializerEntry.getKey(), jsonDeserializerEntry.getValue());
        }

        return jacksonMapperBuilder.build();
    }


    public void insert(D document) {
        MongoCollection mongoCollection = jongo.getCollection(collectionName);
        mongoCollection.insert(document);
    }

    public List<D> findAll() {
        MongoCollection mongoCollection = jongo.getCollection(collectionName);
        final Iterable<? extends D> mongoDBDocuments = mongoCollection.find().as(documentClass);
        final Iterator<? extends D> mongoDBDocumentIterator = mongoDBDocuments.iterator();
        if (!mongoDBDocumentIterator.hasNext()) {
            return Collections.EMPTY_LIST;
        }

        List<D> mongoDBDocumentList = new ArrayList<D>();
        for (D mongoDBDocument : mongoDBDocuments) {
            mongoDBDocumentList.add(mongoDBDocument);
        }

        return mongoDBDocumentList;
    }

    public void update(Object entityId, D newDocument) {
        MongoCollection mongoCollection = jongo.getCollection(collectionName);
        mongoCollection.update(getJsonDocument(entityId)).with(newDocument);
    }

    public D findById(Object entityId) {
        MongoCollection mongoCollection = jongo.getCollection(collectionName);
        Iterator<? extends D> allDocumentIterator = mongoCollection.find(getJsonDocument(entityId)).as(documentClass).iterator();
        if (!allDocumentIterator.hasNext()) {
            throw new NoSuchDocumentException();
        }

        D document = allDocumentIterator.next();
        if (allDocumentIterator.hasNext()) {
            throw new DuplicateDocumentException();
        }

        return document;
    }

    private String getJsonDocument(Object document) {
        StringWriter stringWriter = new StringWriter();
        try {
            this.objectMapper.writeValue(stringWriter, document);
        } catch (IOException ioe) {
            throw new MongoDBException(ioe);
        }
        return stringWriter.toString();
    }
}
